﻿using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Text;

namespace ES.Transfer
{
    /// <summary>
    /// Md5校验类
    /// 大文件校验使用 分片Md5检验
    /// </summary>
    public class FileValidate
    {
        /// <summary>
        /// 
        /// </summary>
        /// <param name="filePath1">第1个文件路径</param>
        /// <param name="filePath2">第2个文件路径</param>
        /// <param name="chunkLength">每次随机抽取的字节数组大小,默认15MB</param>
        /// <param name="gbValidateCount">超大文件时，每GB的校验次数</param>
        /// <param name="valiteType">超大文件，检验方式（1:md5方式, 2:字节数组方式，3：ReadOnlySpan<byte>方式，4：memcmp方式）</param>
        public FileValidate(string filePath1, string filePath2, int chunkLength = 1024 * 1024 * 15, int gbValidateCount = 3, int valiteType = 4)
        {
            this.FInfo1 = new FileInfo(filePath1);
            this.FInfo2 = new FileInfo(filePath2);

            this.MD5Provider = new MD5CryptoServiceProvider();

            this.ChunkLength = chunkLength;

            this.GBValidateCount = gbValidateCount;

            this.ValiteType = valiteType;


            //if (this.FInfo1.Length <= (1024 * 1024 * 1024) * (long)50)
            //{
            //    long yShu = (FInfo1.Length % (1024 * 1024 * 1024));

            //    this.TotalValidateCount = (yShu > 0) ?
            //        (FInfo1.Length / (1024 * 1024 * 1024) * this.GBValidateCount + 1) : (FInfo1.Length / (1024 * 1024 * 1024) * this.GBValidateCount);

            //}
        }

        /// <summary>
        /// 文件1
        /// </summary>
        public FileInfo FInfo1 { get; private set; }

        /// <summary>
        /// 文件2
        /// </summary>
        public FileInfo FInfo2 { get; private set; }

        /// <summary>
        /// 大文件校验时，分片大小
        /// </summary>
        public int ChunkLength { get; private set; }


        /// <summary>
        /// 大文件检验时，每GB检验次数
        /// </summary>
        public int GBValidateCount { get; set; }

        /// <summary>
        /// 大文件校验时，总共校验次数
        /// </summary>
        public long TotalValidateCount { get; private set; } = 1;

        /// <summary>
        ///大文件校验时 使用MD5服务类
        /// </summary>
        public MD5CryptoServiceProvider MD5Provider { get; set; }

        /// <summary>
        /// 检验方式（1:md5方式, 2:字节数组方式，3：ReadOnlySpan<byte>方式，4：memcmp方式）
        /// </summary>
        public int ValiteType { get; set; }

        /// <summary>
        /// 文件1的MD5 （注：计算大文件MD5值将会很耗时)
        /// </summary>
        public string MD51
        {
            get
            {
                return ComputeMD5(this.FInfo1.FullName);
            }
        }

        /// <summary>
        /// 文件2的MD5 （注：计算大文件MD5值将会很耗时)
        /// </summary>
        public string MD52
        {
            get
            {
                return ComputeMD5(this.FInfo2.FullName);
            }
        }

        /// <summary>
        /// 检测是否相同
        /// </summary>
        /// <returns></returns>
        public bool IsSame()
        {
            if (!this.FInfo1.Exists || !this.FInfo2.Exists)
            {
                return false;
            }

            if (this.FInfo1.Length != this.FInfo2.Length)
            {
                return false;
            }

            if (this.FInfo1.Length <= (1024 * 1024 * 1024)) //1GB 以内使用 全文件 Md5验证
            {
                return MD51.Equals(MD52);
            }
            //else if (this.fileInfo1.Length <= (1024 * 1024 * 1024 * (long)50))// 50GB 以内使用 全文件 memcmp方式 字节数组比较 （生产环境时 可取消该段代码注释）
            //{
            //    using (FileStream fs1 = File.Open(this.fileInfo1.FullName, FileMode.Open))
            //    {
            //        using (FileStream fs2 = File.Open(this.fileInfo2.FullName, FileMode.Open))
            //        {
            //            byte[] buffer1 = null, buffer2 = null;
            //            buffer1 = new byte[ChunkLength];
            //            buffer2 = new byte[ChunkLength];

            //            while (true)
            //            {
            //                var realLen1 = fs1.Read(buffer1, 0, this.ChunkLength);

            //                var realLen2 = fs2.Read(buffer2, 0, this.ChunkLength);

            //                if (realLen1 == 0 || realLen2 == 0)
            //                {
            //                    break;
            //                }

            //                if (Memcmp(buffer1, buffer2) != 0)
            //                {
            //                    return false;
            //                }
            //            }
            //        }
            //    }
            //    return true;
            //}
            else //使用3次随机的 字节片段 抽取 进行Md5 验证
            {
                long yShu = (FInfo1.Length % (1024 * 1024 * 1024));

                this.TotalValidateCount = (yShu > 0) ?
                    (FInfo1.Length / (1024 * 1024 * 1024) * this.GBValidateCount + 1) : (FInfo1.Length / (1024 * 1024 * 1024) * this.GBValidateCount);

                for (int j = 0; j < this.TotalValidateCount; j++)
                {
                    var lg = NextLong(0, FInfo1.Length - ChunkLength);//随机取片
                    var bl = CheckChunkReadMD5(this.FInfo1.FullName, this.FInfo2.FullName, lg);
                    if (!bl)
                    {
                        return false;
                    }
                }
                return true;
            }
        }

        private static Random Rdom = new Random();
        private long NextLong(long minValue, long maxValue)
        {
            if (minValue > maxValue)
            {
                throw new ArgumentException("minValue is great than maxValue", "minValue");
            }
            long num = maxValue - minValue;
            return minValue + (long)(Rdom.NextDouble() * num);
        }

        private string ComputeMD5(string filePath)
        {
            byte[] buffer = new byte[this.ChunkLength];
            Stream inputStream = File.Open(filePath, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite);
            HashAlgorithm hashAlgorithm = new MD5CryptoServiceProvider();
            int readLength = 0;//每次读取长度
            var output = new byte[this.ChunkLength];
            while ((readLength = inputStream.Read(buffer, 0, buffer.Length)) > 0)
            {
                hashAlgorithm.TransformBlock(buffer, 0, readLength, output, 0);
            }
            hashAlgorithm.TransformFinalBlock(buffer, 0, 0);
            string md5 = BitConverter.ToString(hashAlgorithm.Hash);
            hashAlgorithm.Clear();
            inputStream.Close();
            inputStream.Dispose();
            return md5;
        }

        private bool CheckChunkReadMD5(string filePath1, string filePath2, long skip)
        {
            FileStream fs1 = null, fs2 = null;
            byte[] buffer1 = null, buffer2 = null;

            fs1 = File.Open(filePath1, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite);
            buffer1 = new byte[this.ChunkLength];
            fs1.Position = skip;
            fs1.Read(buffer1, 0, buffer1.Length);
            fs1.Close();
            fs1.Dispose();

            fs2 = File.Open(filePath2, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite);
            buffer2 = new byte[this.ChunkLength];
            fs2.Position = skip;
            fs2.Read(buffer2, 0, buffer2.Length);
            fs2.Close();
            fs2.Dispose();

            if (ValiteType == 1)
            {
                var str1 = BitConverter.ToString(this.MD5Provider.ComputeHash(buffer1));
                var str2 = BitConverter.ToString(this.MD5Provider.ComputeHash(buffer2));
                return str1.Equals(str2);
            }
            else if (ValiteType == 2)
            {
                return BytesCompare(buffer1, buffer2) == 0;
            }
            else if (ValiteType == 3)
            {
                //https://www.cnblogs.com/waku/p/11069214.html
                //https://blog.csdn.net/liu_if_else/article/details/74332595

                return ((ReadOnlySpan<byte>)buffer1).SequenceEqual((ReadOnlySpan<byte>)buffer2);
            }
            else if (ValiteType == 4)
            {
                return Memcmp(buffer1, buffer2) == 0;
            }

            return false;
        }

        #region 比较字节数组的2这种方式 https://www.cnblogs.com/zgqys1980/archive/2009/07/13/1522546.html

        /// <summary>
        /// 比较字节数组
        /// </summary>
        /// <param name="b1">字节数组1</param>
        /// <param name="b2">字节数组2</param>
        /// <returns>如果两个数组相同，返回0；如果数组1小于数组2，返回小于0的值；如果数组1大于数组2，返回大于0的值。</returns>
        public static int BytesCompare(byte[] b1, byte[] b2)
        {
            int result = 0;
            if (b1.Length != b2.Length)
                result = b1.Length - b2.Length;
            else
            {
                for (int i = 0; i < b1.Length; i++)
                {
                    if (b1[i] != b2[i])
                    {
                        result = (int)(b1[i] - b2[i]);
                        break;
                    }
                }
            }
            return result;
        }


        /// <summary>
        /// memcmp API
        /// </summary>
        /// <param name="b1">字节数组1</param>
        /// <param name="b2">字节数组2</param>
        /// <returns>如果两个数组相同，返回0；如果数组1小于数组2，返回小于0的值；如果数组1大于数组2，返回大于0的值。</returns>
        [DllImport("msvcrt.dll", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]
        private static extern IntPtr memcmp(byte[] b1, byte[] b2, IntPtr count);

        /// <summary>
        /// 用memcmp比较字节数组
        /// </summary>
        /// <param name="b1">字节数组1</param>
        /// <param name="b2">字节数组2</param>
        /// <returns>如果两个数组相同，返回0；如果数组1小于数组2，返回小于0的值；如果数组1大于数组2，返回大于0的值。</returns>
        public static int Memcmp(byte[] b1, byte[] b2)
        {
            IntPtr retval = memcmp(b1, b2, new IntPtr(b1.Length));
            return retval.ToInt32();
        }

        #endregion


    }
}
